package org.mashart.pink.m.gps;

import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;
import java.util.Timer;
import java.util.TimerTask;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
//import javax.bluetooth.DataElement;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import org.mashart.pink.m.gps.data.NMEAAssembler;

/**
 * The only implementation of <code>DeviceConnector</code> interface
 * for the moment.
 *
 * Discovers BT devices and searches for GPS service
 * (poor implementation at the moment)
 */
public class BtConnector extends TimerTask implements DeviceConnector, DiscoveryListener {

    /**
     * Accuracy of the bluetooth GPS device.
     * Refer to the specification of the GPS device.
     * If you don't know it, the common bluetooth GPS should has the accuracy of 10 meters.
     */
    private final int ACCURACY = 10;
    /**
     * Milliseconds between two scans for NMEA sentence
     */
    private final int INTERVAL = 100;
    /**
     * Reconnect interval, reconnect when continous failure reach this interval.
     * Use to limit the reconnect frequency
     */
    private final int FAILURE_BEFORE_RECONNECT = 4;

    /**
     * <code>true</code> if this instance has been configured already
     * @see <code>configure()</code>
     */
    private boolean configured;
    /**
     * Contains NMEA sentence string
     * Used in <code>getRaw()</code>
     */
    private byte[] buffer;
    /**
     * Semaphore, notify when inquiry completed
     */
    private final Object semInquiry;
    /**
     * Semaphore, notify when service search completed
     */
    private final Object semServiceSearch;
    /**
     * Semaphore, notify when "run" stopped
     */
    private final Object sem;
    /**
     * NMEA assembler, put the NMEA sentences to it, and get waypoints from it.
     */
    private NMEAAssembler assembler;
    /**
     * switch, if true, the bluetooth connector is scanning for NMEA sentence and assembling waypoints
     */
    private boolean active;
    /**
     * Timer
     */
    private Timer timer;
    /**
     * Counter of continous failures when reading remote device
     */
    private int failureCounter;

    /**
     * true - we got GPS data streaming via BT
     * false - something went wrong
     */
    private boolean openedStream;
    /**
     * Bluetooth stream connection with a GPS device
     */
    private StreamConnection con;
    /**
     * Input stream for GPS data
     */
    private InputStream in;
    /**
     * Local bluetooth device
     */
    private LocalDevice ld;
    /**
     * Remote bluetooth device in use
     */
    private RemoteDevice rd;
    /**
     * All the discovered remote devices
     */
    private Vector remoteDevices;
    /**
     * Names of all the discovered remote devices
     */
    private Vector deviceNames;
    /**
     * used to discover remote devices
     */
    private DiscoveryAgent discoveryAgent;
    /**
     * URL of the remote device in use
     */
    private String btConnectionURL;

    // 0x1101 is the UUID for the Serial Profile
    private UUID[] uuidSet = {new UUID(0x1101)};

    // 0x0100 is the attribute for the service name element
    // in the service record
    private int[] attrSet = {0x0100};

    /**
     * Constructor
     */
    public BtConnector() {
        semInquiry = new Object();
        semServiceSearch = new Object();
        sem = new Object();
        buffer = new byte[200];
        remoteDevices = new Vector();
        deviceNames = new Vector();

        configured = false;
        active = false;
    }

    /**
     * Preparation for <code>initialize()</code>
     */
    public int configure() {
        assembler = new NMEAAssembler(ACCURACY);
        configured = true;
        return 1;
    }

    /**
     * Check if the Bluetooth API is available
     */
    public static boolean isAPIAvailable(){
        if( System.getProperty("bluetooth.api.version") != null ){
            return true;
        }else{
            try{
                Class.forName("javax.bluetooth.LocalDevice");
                return LocalDevice.getProperty("bluetooth.api.version") != null;
            }catch(Exception ex){
                return false;
            }
        }
    }

    /**
     * Get the remote devices in vicinity
     */
    public void discoverRemoteDevices() {
        /* configure() method should be called first */
        if (!configured) {
            return;
        }

        // initialize
        remoteDevices.removeAllElements();
        deviceNames.removeAllElements();

        synchronized (semInquiry) {
            try {
                ld = LocalDevice.getLocalDevice();
                discoveryAgent = ld.getDiscoveryAgent();
                /* initiate BT discovery */
                discoveryAgent.startInquiry(DiscoveryAgent.GIAC, this);
            } catch (BluetoothStateException e) {
//                e.printStackTrace();
                return;
            /* TODO better error handling, friendly error messages */
            }
            try {
                /**
                 * Wait for the discovery to finish.
                 * We should have Stream connection opened passed this point */
                semInquiry.wait();
            } catch (InterruptedException e) {
//                e.printStackTrace();
            // J2ME threads can't be interrupted
            }
        }
    }

    /**
     * Get the names of discovered remote devices.
     * Should be called after <code>discoverRemoteDevices()</code>.
     * @return a vector containing the names of remote devices.
     * The order of names is the same as in vector <code>remoteDevices</code>.
     */
    public Vector getRemoteDeviceNames() {
        return deviceNames;
    }

    /**
     * Set the remote device to be used
     */
    public void setRemoteDevice(int selectedIndex) {
        rd = (RemoteDevice) remoteDevices.elementAt(selectedIndex);
        synchronized (semServiceSearch) {
            try {
                discoveryAgent.searchServices(attrSet, uuidSet, rd, this);
            } catch (BluetoothStateException e) {
            }

            try {
                /**
                 * Wait for the discovery to finish.
                 * We should have Stream connection opened passed this point */
                semServiceSearch.wait();
            } catch (InterruptedException e) {
            // J2ME threads can't be interrupted
            }
        }
    }

    /**
     * Get the bluetooth connection URL
     */
    public String getBtConnectoinURL() {
        return btConnectionURL;
    }

    /**
     * Set the bluetooth connection URL
     */
    public void setBtConnectoinURL(String url) {
        btConnectionURL = url;
    }

    /**
     * Connect the BT device
     */
    private int connect() {
//        if (ld == null) {
//            discover();
//        }
        try {
            con = (StreamConnection) Connector.open(btConnectionURL);
            in = con.openInputStream();
            openedStream = true;
        } catch (IOException ex) {
            openedStream = false;
        }
        return openedStream ? 1 : -1;
    }

    /**
     * Starts BT devices dicovery and should eventually establish
     * BT connection to the GPS device
     * @see <code>inquiryCompleted()</code>
     */
    public int initialize() {
        connect();

        // set the status flag
        active = false;
        // reset the failure counter
        failureCounter = 0;
        // start the timer
        if (openedStream) {
            timer = new Timer();
            timer.schedule(this, 0, INTERVAL);
        }
        return openedStream ? 1 : -1;
    }

    /**
     * Activate
     */
    public int activate() {
        if (active) {
            return 0;
        }
        active = true;
//        timer = new Timer();
//        timer.schedule(this, 0, interval);
        return 1;
    }

    /**
     * Deactivate
     */
    public int deactivate() {
        if (!active) {
            return 0;
        }
//        timer.cancel();
//        waitTilFinish();
        active = false;
        return 1;
    }

    /**
     * @return NMEA sentence, e.g.
     * "$GPGGA,161229.487,3723.2475,N,12158.3416,W,1,07,1.0,9.0,M,,,,0000*18"
     */
    public String getRaw() {
        int len = 0;
        int chr;
        int fin = 0;
        String sentence;

        try {
            while (fin < 2 && len < buffer.length) {
                /* read one character at a time */
                chr = in.read();
                switch (chr) {

                    /* Line Feed: NMEA sentence should end here */
                    case 10:
                        if (fin == 1) {
                            fin = 2;
                        }
                        break;

                    /* Carriage Return: we now need the last char, Line Feed */
                    case 13:
                        fin = 1;
                        break;

                    /* Blank space: we don't need that.
                     * shouldn't happen anyway */
                    case 32:
                        fin = 0;
                        break;

                    /* all the rest are useful characters */
                    default:
                        fin = 0;
                        buffer[len++] = (byte) chr;
                }
            }
            sentence = new String(buffer, 0, len);
        } catch (IOException ex) {
            sentence = null;
        }

        return sentence;
    }

    /**
     * @return a Waypoint
     */
    public Waypoint getWaypoint() {
        return assembler.get();
    }

    /**
     * Thread entry point. Receives the incoming NMEA sentences from bluetooth device and assembles the waypoint.
     */
    public void run() {
        synchronized (sem) {
            if (active) {
                // try to read raw NMEA sentence
                String sentence = getRaw();
                // if failed to get sentence from the BT device
                if (sentence == null) {
                    failureCounter++;
                    if (failureCounter >= FAILURE_BEFORE_RECONNECT) {
                        // reconnect
                        connect();
                        // reset the counter
                        failureCounter = 0;
                    }
                } else {
                    // push the NMEA sentence into the assembler
                    assembler.put(sentence);
                }
            }
        }
    }

    /**
     * Closes <code>InputStream</code> and <code>StreamConnection</code>
     */
    public int terminate() {
        if (active) {
            // cancel scheduled scans
            deactivate();
        }

        // cancel the timer, which is scheduled in initialze()
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
        // wait for the scanning thread to finish
        waitTilFinish();

        boolean term = true;
        if( in != null ){
            try {
                in.close();
            } catch (IOException ex) {
                term = false;
            }
        }
        if( con != null ){
            try {
                con.close();
            } catch (IOException ex1) {
                term = false;
            }
        }

        ld = null;
        rd = null;

        return term ? 1 : -1;
    }

    /**
     * Wait for the NMEA message scanning thread to finish
     */
    public void waitTilFinish() {
        synchronized (sem) {
            // active should be set to false before calling waitTilFinish()
            // here set it again, just for fault tolerance
            active = false;
        }
    }

    /*******************************************************
     * DeviceConnector interface methods
     */
    /**
     * Callback for every BT device discovered during inquiry
     */
    public void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod) {
        remoteDevices.addElement(btDevice);
        String deviceName;
        try {
            deviceName = btDevice.getFriendlyName(false);
        } catch (IOException e) {
            deviceName = "Unknown Device";
        }
        deviceNames.addElement(deviceName);
    }

    /**
     * Callback for every service discovered on a BT device
     * We don't use this for the moment.
     * TODO should we?
     */
    public void servicesDiscovered(int transID, ServiceRecord[] servRecord) {
        for (int i = 0; i < servRecord.length; i++) {
//            DataElement serviceNameElement = servRecord[i].getAttributeValue(attrSet[0]);
//            String _serviceName = (String)serviceNameElement.getValue();
//            String serviceName = _serviceName.trim();
            // if more than one services are found, just record the last one
            btConnectionURL = servRecord[i].getConnectionURL(ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
        }
    }

    /**
     * Callback for completion of a BT services discovery
     */
    public void serviceSearchCompleted(int transID, int respCode) {
        if (respCode == DiscoveryListener.SERVICE_SEARCH_COMPLETED) {
            // the service search process was successful
            synchronized (semServiceSearch) {
                // nofity we're done here
                semServiceSearch.notify();
            }
        } else {
            // the service search process has failed
        }
    }

    /**
     * Callback for completion of <code>startInquiry()</code>
     * We should be ready to open a StreamConnection now
     */
    public void inquiryCompleted(int discType) {
        synchronized (semInquiry) {
            // nofity we're done here
            semInquiry.notify();
        }
    }
}